package com.p2p.base.page;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.jdbc.SqlBuilder;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;

/**
 * Mybatis分页拦截器。 拦截器=手动sql+自动生成sql工具类。
 * 
 * @author fansunion@qq.com
 * 
 */
@Intercepts({ @Signature(method = "prepare", type = StatementHandler.class, args = { Connection.class }) })
public class MybatisPageInterceptor implements Interceptor {

	private String databaseType;

	/**
	 * 拦截后要执行的方法
	 */
	public Object intercept(Invocation invocation) throws Throwable {
		// 对于StatementHandler其实只有两个实现类，一个是RoutingStatementHandler，另一个是抽象类BaseStatementHandler，
		// BaseStatementHandler有三个子类，分别是SimpleStatementHandler，PreparedStatementHandler和CallableStatementHandler，
		// SimpleStatementHandler是用于处理Statement的，PreparedStatementHandler是处理PreparedStatement的，而CallableStatementHandler是
		// 处理CallableStatement的。

		// Mybatis在进行Sql语句处理的时候都是建立的RoutingStatementHandler，而在RoutingStatementHandler里面拥有一个
		// StatementHandler类型的delegate属性，RoutingStatementHandler会依据Statement的不同建立对应的BaseStatementHandler，即SimpleStatementHandler、
		// PreparedStatementHandler或CallableStatementHandler，在RoutingStatementHandler里面所有StatementHandler接口方法的实现都是调用的delegate对应的方法。
		// 我们在PageInterceptor类上已经用@Signature标记了该Interceptor只拦截StatementHandler接口的prepare方法，又因为Mybatis只有在建立RoutingStatementHandler的时候
		// 是通过Interceptor的plugin方法进行包裹的，所以我们这里拦截到的目标对象肯定是RoutingStatementHandler对象。
		RoutingStatementHandler handler = (RoutingStatementHandler) invocation
				.getTarget();
		// 通过反射获取到当前RoutingStatementHandler对象的delegate属性
		StatementHandler delegate = (StatementHandler) ReflectUtil
				.getFieldValue(handler, "delegate");
		// 获取到当前StatementHandler的
		// boundSql，这里不管是调用handler.getBoundSql()还是直接调用delegate.getBoundSql()结果是一样的，因为之前已经说过了
		// RoutingStatementHandler实现的所有StatementHandler接口方法里面都是调用的delegate对应的方法。
		BoundSql boundSql = delegate.getBoundSql();
		// 拿到当前绑定Sql的参数对象，就是我们在调用对应的Mapper映射语句时所传入的参数对象
		Object obj = boundSql.getParameterObject();
		// 这里我们简单的通过传入的是Page对象就认定它是需要进行分页操作的。
		if (obj instanceof PageVo) {
			PageVo page = (PageVo) obj;
			//检查分页参数的合法性，防止被攻击
			page.check();
			// 通过反射获取delegate父类BaseStatementHandler的mappedStatement属性
			MappedStatement mappedStatement = (MappedStatement) ReflectUtil
					.getFieldValue(delegate, "mappedStatement");
			// 拦截到的prepare方法参数是一个Connection对象
			Connection connection = (Connection) invocation.getArgs()[0];
			// 获取当前要执行的Sql语句，也就是我们直接在Mapper映射语句中写的Sql语句
			String sql = boundSql.getSql();
			// 给当前的page参数对象设置总记录数
			this.setTotalRecord(page, mappedStatement, connection);
			// 获取分页Sql语句
			String pageSql = getPageSql(page, sql, true);
			// 利用反射设置当前BoundSql对应的sql属性为我们建立好的分页Sql语句
			ReflectUtil.setFieldValue(boundSql, "sql", pageSql);
		}
		// 是否有必要把dao返回的list放到page的rows中呢？service层可能会对dao返回的list进行再次处理
		return invocation.proceed();
	}

	/**
	 * 拦截器对应的封装原始对象的方法
	 */
	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}

	/**
	 * 设置注册拦截器时设定的属性
	 */
	public void setProperties(Properties properties) {
		this.databaseType = properties.getProperty("databaseType");
	}

	/**
	 * 获取Mysql数据库的分页查询语句
	 * 
	 * @param page
	 *            分页对象
	 * @param sqlBuffer
	 *            包含原sql语句的StringBuffer对象 * @param isOrderAndLimit 是否需要带上排序和分页
	 * @return Mysql数据库分页语句
	 */
	private String getPageSql(PageVo page, String sql, boolean isOrderAndLimit) {
		StringBuffer sqlBuffer = new StringBuffer(sql);
		// 计算第一条记录的位置，Mysql中记录的位置是从0开始的。
		Map<String, String> query = page.getParams();

		if (query != null) {
			Set<String> keySet = query.keySet();
			for (String searchKey : keySet) {
				String searchValue = query.get(searchKey);
				String value = searchValue.toString().trim();
				if (!value.equals("")) {
					int whereIndex = sqlBuffer.indexOf("where");
					int orderByIndex = sqlBuffer.indexOf("order by");
					String cdt = " = ";
					// like-开始表示模糊查询
					String flagLike = "like_";
					String flagGreatThanEqual="ge_";
					if (searchKey.startsWith(flagLike)) {
						cdt = " like ";
						searchKey = searchKey.substring(flagLike.length());
						// 对搜索词中的MySQL关键字进行转义,$1是指对应的符号，比如'_'被转义为'\_'
						value = " '%" + value.replaceAll("([%_'\"])", "\\\\$1")
								+ "%' ";
					}else if(searchKey.startsWith(flagGreatThanEqual)){
						cdt =" >= ";
						searchKey = searchKey.substring(flagGreatThanEqual.length());
						value = "'"+value+"'";
					}
					else{
						value="'"+value+"'";
					}
		
					// 原有sql中不存在where
					String whereSql = " where " + searchKey + cdt + value + " ";
					if (whereIndex == -1) {
						if (orderByIndex == -1) {
							sqlBuffer.append(whereSql);
						} else {
							// order 应该在where后面
							String selectSql = sqlBuffer.substring(0,
									orderByIndex);

							String orderSql = sqlBuffer.substring(orderByIndex);

							StringBuffer newStringBuffer = new StringBuffer();
							newStringBuffer.append(selectSql);
							newStringBuffer.append(whereSql);
							newStringBuffer.append(orderSql);
							sqlBuffer = newStringBuffer;
						}
					} else {
						// 通过替换的方式，不需要判断and,这里的查询条件放在已有条件的“前面”，所以可以直接带“and”
						sqlBuffer = sqlBuffer.replace(whereIndex,
								whereIndex + 5, whereSql + " and ");
					}
				}
			}
		}
		if (isOrderAndLimit) {
		/*	// 排序,sql中的排序优先级高于外部传入的排序
			String orderBy = page.getOrderBy();
			int innerOrderExist = sqlBuffer.indexOf("order by");
			// 内部+外部，至少2个排序字段
			if (innerOrderExist != -1) {
				if (StringUtils.isNotEmpty(orderBy)) {
					sqlBuffer.append(",").append(orderBy)
							.append(" " + page.getOrder());
				}
			}
			// 外部，1个排序字段
			else {
				if (StringUtils.isNotEmpty(orderBy)) {
					sqlBuffer.append("order by ").append(orderBy)
							.append(" " + page.getOrder());
				}
			}*/

			// 分页
			sqlBuffer.append(" limit ").append(page.getFirstResult())
					.append(",").append(page.getMaxResults());
		}
		return sqlBuffer.toString();
	}

	/**
	 * 给当前的参数对象page设置总记录数
	 * 
	 * @param page
	 *            Mapper映射语句对应的参数对象
	 * @param mappedStatement
	 *            Mapper映射语句
	 * @param connection
	 *            当前的数据库连接
	 */
	private void setTotalRecord(PageVo page, MappedStatement mappedStatement,
			Connection connection) {
		// 获取对应的BoundSql，这个BoundSql其实跟我们利用StatementHandler获取到的BoundSql是同一个对象。
		// delegate里面的boundSql也是通过mappedStatement.getBoundSql(paramObj)方法获取到的。
		BoundSql boundSql = mappedStatement.getBoundSql(page);
		// 获取到我们自己写在Mapper映射语句中对应的Sql语句
		String sql = boundSql.getSql();
		String pageSql = getPageSql(page, sql, false);
		// 通过查询Sql语句获取到对应的计算总记录数的sql语句
		String countSql = getCountSql(pageSql);
		// 通过BoundSql获取对应的参数映射
		List<ParameterMapping> parameterMappings = boundSql
				.getParameterMappings();
		// 利用Configuration、查询记录数的Sql语句countSql、参数映射关系parameterMappings和参数对象page建立查询记录数对应的BoundSql对象。
		BoundSql countBoundSql = new BoundSql(
				mappedStatement.getConfiguration(), countSql,
				parameterMappings, page);
		// 通过mappedStatement、参数对象page和BoundSql对象countBoundSql建立一个用于设定参数的ParameterHandler对象
		ParameterHandler parameterHandler = new DefaultParameterHandler(
				mappedStatement, page, countBoundSql);
		// 通过connection建立一个countSql对应的PreparedStatement对象。
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		try {
			pstmt = connection.prepareStatement(countSql);
			// 通过parameterHandler给PreparedStatement对象设置参数
			parameterHandler.setParameters(pstmt);
			// 之后就是执行获取总记录数的Sql语句和获取结果了。
			rs = pstmt.executeQuery();
			if (rs.next()) {
				int totalCount = rs.getInt(1);
				// 给当前的参数page对象设置总记录数
				page.setTotalCount(totalCount);
				Integer totalPage = PageUtil.caculateTotalPage(totalCount,
						page.getPageSize());
				page.setPages(totalPage);
			}
		} catch (SQLException e) {
			page.setTotalCount(0);
			e.printStackTrace();
		} finally {
			try {
				if (rs != null) {
					rs.close();
				}
				if (pstmt != null) {
					pstmt.close();
				}
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * 根据原Sql语句获取对应的查询总记录数的Sql语句
	 * 
	 */
	private String getCountSql(String pageSql) {
		int index = pageSql.indexOf("from");
		return "select count(*) " + pageSql.substring(index);
	}

	/**
	 * 利用反射进行操作的一个工具类
	 * 
	 */
	private static class ReflectUtil {
		/**
		 * 利用反射获取指定对象的指定属性
		 * 
		 * @param obj
		 *            目标对象
		 * @param fieldName
		 *            目标属性
		 * @return 目标属性的值
		 */
		public static Object getFieldValue(Object obj, String fieldName) {
			Object result = null;
			Field field = ReflectUtil.getField(obj, fieldName);
			if (field != null) {
				field.setAccessible(true);
				try {
					result = field.get(obj);
				} catch (IllegalArgumentException e) {
					e.printStackTrace();
				} catch (IllegalAccessException e) {
					e.printStackTrace();
				}
			}
			return result;
		}

		/**
		 * 利用反射获取指定对象里面的指定属性
		 * 
		 * @param obj
		 *            目标对象
		 * @param fieldName
		 *            目标属性
		 * @return 目标字段
		 */
		private static Field getField(Object obj, String fieldName) {
			Field field = null;
			for (Class<?> clazz = obj.getClass(); clazz != Object.class; clazz = clazz
					.getSuperclass()) {
				try {
					field = clazz.getDeclaredField(fieldName);
					break;
				} catch (NoSuchFieldException e) {
					// 这里不用做处理，子类没有该字段可能对应的父类有，都没有就返回null。
				}
			}
			return field;
		}

		/**
		 * 利用反射设置指定对象的指定属性为指定的值
		 * 
		 * @param obj
		 *            目标对象
		 * @param fieldName
		 *            目标属性
		 * @param fieldValue
		 *            目标值
		 */
		public static void setFieldValue(Object obj, String fieldName,
				String fieldValue) {
			Field field = ReflectUtil.getField(obj, fieldName);
			if (field != null) {
				try {
					field.setAccessible(true);
					field.set(obj, fieldValue);
				} catch (IllegalArgumentException e) {
					e.printStackTrace();
				} catch (IllegalAccessException e) {
					e.printStackTrace();
				}
			}
		}
	}

}